/**
 * 
 */
package pacMan;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

import javax.swing.JPanel;

import pacMan.GameStateManager.GAME_BACKGROUND_SOUND;
import pacMan.GameStateManager.GAME_EVENT;

/**
 * Class: GameBoard
 * This is the "model" part of the application, based on the MVC pattern. Provides functionality for
 * manipulating the size and location of objects that are managed by the ObjectLocationTracker. This
 * has a partner class GameStateManager that encapsulates handling of all game events and actions to
 * be taken on each state transition.
 * 
 * @author 		Tomas Rivera
 * @version 	$Id$
 *
 */
@SuppressWarnings("serial")
public class GameBoard extends JPanel {
	
	//////////////////////////////////////////////////////////
	// Constructor methods
	//////////////////////////////////////////////////////////
	
	/**
	 * Creates a GameBoard object that manages location of
	 * all objects that are represented in the application, as
	 * well as allowing for manipulation of location, size and
	 * other attributes of each object.
	 * 
	 * Note: assumes that game board will always be 27x30 tiles.
	 * 
	 * @param windowParams
	 * @param scoreTracker
	 * @param objectLocationTracker
	 */
	public GameBoard(WindowParameters windowParams, ScoreTracker scoreTracker, ObjectLocationTracker objectLocationTracker) {
		this.windowParams = windowParams;
		this.scoreTracker = scoreTracker;
		this.objectLocationTracker = objectLocationTracker;
		this.gameStateManager = new GameStateManager(this);
		
		// TODO need to be able to scale font as screen size changes.
		textFont = new Font("Lucida Console", Font.BOLD, 24);
		
		enemyActors = new ArrayList<Actor>();
		gamePaused = false;
		
		initializeActors();
		updateMovesPerFrame(gameStateManager.getCurrentGameRound());
		frameCounter = 0;
		ghostsFlashing = false;
		gameStateManager.handleEvent(GameStateManager.GAME_EVENT.GAME_BOARD_INITIALIZED);
	}
	
	//////////////////////////////////////////////////////////
	// Public / Protected methods
	//////////////////////////////////////////////////////////
	
	/**
	 * Get list of enemy Actors.
	 * 
	 * @return
	 */
	public ArrayList<Actor> getEnemyActors() { return enemyActors; }
	
	/**
	 * Get remaining number of times player can be killed before we declare the
	 * game to be over.
	 * 
	 * @return
	 */
	public int getPlayerLivesRemaining() {
		return playerObject.getLivesRemaining();
	}
	
	/**
	 * Get a count of the pellets on the game board that have not yet been consumed.
	 * 
	 * @return
	 */
	public int getPelletCountRemaining() { return objectLocationTracker.getPelletCount(); }
	
	/**
	 * Get the total number of pellets that we originally generated when creating the game board.
	 * 
	 * @return
	 */
	public int getPelletCountGenerated() { return objectLocationTracker.getTotalGeneratedCount(); }
	
	/**
	 * Set flag so that ghosts will flip back and forth repeatedly between a set of colors.
	 * 
	 * @param flash
	 */
	public void setGhostsFlashing(boolean flash) {
		ghostsFlashing = flash;
	}
	
	/**
	 * Call the resizeObject() function for all BasicDrawableObjects on the game board.
	 * 
	 */
	public void resizeMaze() {
		playerObject.resizeObject();
		for(Actor enemyActor : enemyActors) {
			enemyActor.resizeObject();
		}
		
		objectLocationTracker.resizeStationaryMazeElements();
		updateMovesPerFrame(gameStateManager.getCurrentGameRound());
	}
	
	/**
	 * Set that the user has requested that the player object movement vector be changed.
	 * 
	 * @param direction
	 */
	public void playerRequestDirectionChange(CardinalDirection direction) {
		playerObject.requestDirectionChange(direction);
	}
	
	/**
	 * Return all Actors to their home locations and set all attributes to their initial states.
	 */
	public void resetActors() {
		for(Actor enemyActor : enemyActors) {
			enemyActor.returnActorToHomeLocation();
			enemyActor.setEdible(false);
			enemyActor.show();
			enemyActor.setHazardous(true);
			enemyActor.setFrozen(0);
		}
		
		playerObject.returnActorToHomeLocation();
		playerObject.show();
		playerObject.setDirection(CardinalDirection.EAST);
		playerObject.setFrozen(0);
	}
	
	/**
	 * Prevent paintComponent() from drawing any of the enemy Actors on the JPanel, and
	 * prevent them from changing their location until further notice.
	 */
	public void hideEnemyActors() {
		for(Actor enemyActor : enemyActors) {
			enemyActor.hide();
			enemyActor.setFrozen(-1);
		}
	}
	
	/**
	 * Prevent all Actors from changing their location for the specified number of repaint() calls.
	 * 
	 * @param frames
	 */
	public void freezeAllActors(int frames) {
		for(Actor enemyActor : enemyActors) {
			enemyActor.setFrozen(frames);
		}
		
		playerObject.setFrozen(frames);
	}
	
	/**
	 * Reset all Actors and consumables on the game board to their initial states and positions.
	 * 
	 */
	public void resetBoardForNextRound() {
		gameStateManager.setMazeComplete(false);
		windowParams.setWallColor(Color.BLUE);
		objectLocationTracker.restoreStationaryConsumables();
	}
	
	/**
	 * Release a Ghost enemy Actor from the pen.
	 * 
	 */
	public void releaseGhost() {
		for(Actor enemyActor : enemyActors) {
			Ghost ghost = (Ghost) enemyActor;
			if(ghost.getMoveMode() == GameStateManager.NAV_MODE.PACE_HOME) {
				ghost.setMoveMode(GameStateManager.NAV_MODE.GO_HOME_CENTER);
				break;
			}
		}
	}
	
	/**
	 * Get the current game state value.
	 * 
	 * @return
	 */
	public GameStateManager.GAME_STATE getGameState() { return gameStateManager.getCurrentGameState(); }

	/**
	 * Start a new game.
	 * 
	 */
	public void startNewGame() {
		scoreTracker.resetScore();
		playerObject.resetLivesRemaining();
		resetBoardForNextRound();
	    gameStateManager.handleEvent(GameStateManager.GAME_EVENT.NEW_GAME_START);
	}
	
	/**
	 * Mute all background music and halt all actions on the game board.
	 * 
	 */
	public void togglePause() {
		gamePaused = !gamePaused;
	}
	
	/**
	 * Set the text message that is centered on the game board.
	 * 
	 * @param message
	 */
	public void setCenterMessage(String message)  {
		centerMessage = message;
	}
	
	/**
	 * Calculates new location for each Actor based on allowed movement parameters
	 * and makes call to repaint objects on screen.
	 * 
	 */
	public void moveActorsAndRepaint() {
		
		synchronized(this) {
			if(!drawLock) {
				drawLock = true;
			} else {
				return;
			}
		}
		
		int currentGameRound = gameStateManager.getCurrentGameRound();
		
		if(gameStateManager.getCurrentGameState() == GameStateManager.GAME_STATE.RUNNING) {
			boolean anyGhostsScared = (blinky.isScared() | pinky.isScared() | inky.isScared() | clyde.isScared());
			boolean allFrozen = (playerObject.isFrozen() & blinky.isFrozen() & inky.isFrozen() & pinky.isFrozen() & clyde.isFrozen());
			
			if(playerObject.isFrozen()) {
				playerObject.decrementFrozen();
			} else {
				int moves = getPlayerMovesThisFrame(gameStateManager.getCurrentGameRound(), anyGhostsScared);
				for(int i = 0; i < moves; i++) {
					gameStateManager.handleEvent(playerObject.moveActor(gameStateManager.getGhostGlobalState()));
					if(!gameStateManager.isGameRunning()) { break; }
				}
			}
			
			if(blinky.isFrozen()) {
				blinky.decrementFrozen();
			} else {
				int moves = getGhostMovesThisFrame(blinky, currentGameRound);
				for(int i = 0; i < moves; i++) {
					gameStateManager.handleEvent(blinky.moveActor(gameStateManager.getGhostGlobalState()));
					if(!gameStateManager.isGameRunning()) { break; }
				}
			}
			
			if(pinky.isFrozen()) {
				pinky.decrementFrozen();
			} else {
				int moves = getGhostMovesThisFrame(pinky, currentGameRound);
				for(int i = 0; i < moves; i++) {
					gameStateManager.handleEvent(pinky.moveActor(gameStateManager.getGhostGlobalState()));
					if(!gameStateManager.isGameRunning()) { break; }
				}
			}
			
			if(inky.isFrozen()) {
				inky.decrementFrozen();
			} else {
				int moves = getGhostMovesThisFrame(inky, currentGameRound);
				for(int i = 0; i < moves; i++) {
					gameStateManager.handleEvent(inky.moveActor(gameStateManager.getGhostGlobalState()));
					if(!gameStateManager.isGameRunning()) { break; }
				}
			}
			
			if(clyde.isFrozen()) {
				clyde.decrementFrozen();
			} else {
				int moves = getGhostMovesThisFrame(clyde, currentGameRound);
				for(int i = 0; i < moves; i++) {
					gameStateManager.handleEvent(clyde.moveActor(gameStateManager.getGhostGlobalState()));
					if(!gameStateManager.isGameRunning()) { break; }
				}
			}
			
			// Check if we need to update the background sound effect
			if(gamePaused) {
				gameStateManager.updateBackgroundSoundState(GAME_BACKGROUND_SOUND.NONE);
			} else if(allFrozen){
				gameStateManager.updateBackgroundSoundState(GAME_BACKGROUND_SOUND.NONE);
			} else if(gameStateManager.getGhostHeadingHomeCount() > 0) {
				gameStateManager.updateBackgroundSoundState(GAME_BACKGROUND_SOUND.GHOST_HEADING_HOME);
			} else if(anyGhostsScared) {
				gameStateManager.updateBackgroundSoundState(GAME_BACKGROUND_SOUND.ENERGIZED);
			} else {
				gameStateManager.startSirenBackgroundEffect();
			}
		} else {
			gameStateManager.updateBackgroundSoundState(GAME_BACKGROUND_SOUND.NONE);
		}
		
		if(frameCounter % 15 == 0) {
			
			if(frameCounter % 30 == 0) {
				if(gameStateManager.mazeComplete()) {
					this.windowParams.setWallColor(Color.WHITE);
				}
			} else {
				if(gameStateManager.mazeComplete()) {
					this.windowParams.setWallColor(Color.BLUE);
				}	
			}
		}
		
		if(frameCounter % 10 == 0) {
			if(ghostsFlashing) {
				blinky.invertScaredColors(true);
				pinky.invertScaredColors(true);
				inky.invertScaredColors(true);
				clyde.invertScaredColors(true);
			}
		} else if(frameCounter % 5 == 0) {
			blinky.invertScaredColors(false);
			pinky.invertScaredColors(false);
			inky.invertScaredColors(false);
			clyde.invertScaredColors(false);
		}
	
		if((frameCounter = (++frameCounter % 60)) == 0) {
			gameStateManager.handleEvent(GAME_EVENT.TIMER_INCREMENT);
		}
		
		this.repaint();
		
		synchronized(this) {
			drawLock = false;
		}
	}
	
	/**
	 * (Debug use only)
	 * Change the current move behavior of all enemy Actors.
	 * 
	 * @param state
	 */
	public void debug_changeGhostState(GameStateManager.MOVE_BEHAVIOR state) {
		gameStateManager.setGhostGlobalState(state);
	}
	
	/**
	 * (Debug use only)
	 * Set/clear all enemy Actors scared state.
	 * 
	 */
	public void debug_toggleEnergizerConsumed() {
		
		// Check if any of the enemy Actors are in "scared" state.
		boolean anyScared = (blinky.edible() || pinky.edible() || inky.edible() || clyde.edible());
		
		if(anyScared) {
			for(Actor enemyActor : enemyActors) {
				enemyActor.setHazardous(true);
				enemyActor.setEdible(false);
			}
		} else {
			for(Actor enemyActor : enemyActors) {
				enemyActor.setHazardous(false);
				enemyActor.setEdible(true);
			}
		}
	}
	
	/**
	 * (Debug use only)
	 * Override game state and declare round complete.
	 * 
	 */
	public void debug_roundComplete() {
		gameStateManager.handleEvent(GameStateManager.GAME_EVENT.DEBUG_ROUND_COMPLETE);
	}
	
	//////////////////////////////////////////////////////////
	// Overridden methods
	//////////////////////////////////////////////////////////
	
	/*
	 * (non-Javadoc)
	 * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
	 */
	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		//paintGridLines(g);

		Graphics2D g2d = (Graphics2D) g;
		
		// Draw all objects contained in the objectLocationTracker.
		objectLocationTracker.drawStationaryMazeElements(g2d);
		
		playerObject.drawObject(g2d);
		
		// Draw all actors
		for(Actor actor : enemyActors) {
			actor.drawObject(g2d);
		}
		
		displayScore(g, scoreTracker.getCurrentScore(), scoreTracker.getHighScore());
		printCenterMessage(g2d);
	}
	
	
	//////////////////////////////////////////////////////////
	// Private methods
	//////////////////////////////////////////////////////////
	
	/*
	 * Initialize all game Actors.
	 * 
	 */
	private void initializeActors() {
		
		blinky = new Ghost(MazeBuilder.createCoordinateObj(13, 11, windowParams), windowParams, 200, objectLocationTracker, Color.RED, "Blinky", 25, -3);

		pinky = new Ghost(MazeBuilder.createCoordinateObj(13, 14, windowParams), this.windowParams, 200, objectLocationTracker, Color.PINK, "Pinky", 2, -3);
		pinky.setFrozen(5);

		inky = new Ghost(MazeBuilder.createCoordinateObj(11, 14, windowParams), this.windowParams, 200, objectLocationTracker, Color.CYAN, "Inky", 27, 31);
		inky.setFrozen(10);
		inky.setPartner(blinky);

		clyde = new Ghost(MazeBuilder.createCoordinateObj(15, 14, windowParams), this.windowParams, 200, objectLocationTracker, Color.ORANGE, "Clyde", 0, 31);
		clyde.setFrozen(15);

		
		enemyActors.add(blinky);
		enemyActors.add(pinky);
		enemyActors.add(inky);
		enemyActors.add(clyde);
		
		
		playerObject = new PlayerObject(
				new Coordinate(
					(13 * windowParams.getPixelScale()) + windowParams.getPixelOffsetX() + (windowParams.getPixelScale() / 2),
					(23 * windowParams.getPixelScale()) + windowParams.getPixelOffsetY(),
					windowParams.getWindowWidth(), windowParams.getWindowHeight()), 
				windowParams, scoreTracker, objectLocationTracker);
		
		System.out.printf("playerMoves = %02f, ghostMoves = %02f%n", playerMovesPerFrame, ghostMovesPerFrame);
	}
	

	/*
	 * Update the general number of pixels that Actor are allowed to move during each
	 * screen redraw call. The actual distance each Actor is allowed to move is modified
	 * based on this initial value.
	 * 
	 */
	private void updateMovesPerFrame(int currentGameRound) {
		actorMovesPerFrame = windowParams.getPixelScale() * 10 / 60;
	}
	
	
	/*
	 * Calculate the number of pixels the playerObject Actor is allowed to move at this time,
	 * based on the current game round and the state of the ghost enemyActors.
	 * 
	 */
	private int getPlayerMovesThisFrame(int currentGameRound, boolean ghostsAreScared) {
		double movesPerFrame;
		int pixelsToMove;
		
		if(currentGameRound == 1) {
			if(ghostsAreScared) {
				movesPerFrame = actorMovesPerFrame * 0.9;
			} else {
				movesPerFrame = actorMovesPerFrame * 0.80;
			}
		} else if(currentGameRound >= 2 && currentGameRound <= 4) {
			if(ghostsAreScared) {
				movesPerFrame = actorMovesPerFrame * 0.95;
			} else {
				movesPerFrame = actorMovesPerFrame * 0.90;
			}
		} else if(currentGameRound >= 5 && currentGameRound <= 20) {
			movesPerFrame = actorMovesPerFrame * 1.0;
		} else {
			movesPerFrame = actorMovesPerFrame * 0.9;
		}
		
		pixelsToMove = (int)Math.floor(movesPerFrame);
		return pixelsToMove + playerObject.incrementMovesPerFrameRemainder(movesPerFrame - pixelsToMove);
	}
	
	
	/*
	 * Calculate the number of pixels the ghost enemy Actors are allowed to move at this time,
	 * based on the current game round and their "frightened" state.
	 * 
	 */
	private int getGhostMovesThisFrame(Ghost ghost, int currentGameRound) {
		double movesPerFrame;
		int pixelsToMove;
		
		if(currentGameRound == 1) {
			if(ghost.getMoveMode() == GameStateManager.NAV_MODE.GO_HOME) {
				movesPerFrame = actorMovesPerFrame * 1.0;
			} else if(objectLocationTracker.objectInTunnel(ghost)) {
				movesPerFrame = actorMovesPerFrame * 0.40;
			} else if(ghost.isScared()) {
				movesPerFrame = actorMovesPerFrame * 0.50;
			} else {
				movesPerFrame = actorMovesPerFrame * 0.75;
			}
		} else if(currentGameRound >= 2 && currentGameRound <= 4) {
			if(ghost.getMoveMode() == GameStateManager.NAV_MODE.GO_HOME) {
				movesPerFrame = actorMovesPerFrame * 1.0;
			} else if(objectLocationTracker.objectInTunnel(ghost)) {
				movesPerFrame = actorMovesPerFrame * 0.55;
			} else if(ghost.isScared()) {
				movesPerFrame = actorMovesPerFrame * 0.45;
			} else {
				movesPerFrame = actorMovesPerFrame * 0.85;
			}
		} else if(currentGameRound >= 5 && currentGameRound <= 20) {
			if(ghost.getMoveMode() == GameStateManager.NAV_MODE.GO_HOME) {
				movesPerFrame = actorMovesPerFrame * 1.0;
			} else if(objectLocationTracker.objectInTunnel(ghost)) {
				movesPerFrame = actorMovesPerFrame * 0.60;
			} else if(ghost.isScared()) {
				movesPerFrame = actorMovesPerFrame * 0.50;
			} else {
				movesPerFrame = actorMovesPerFrame * 0.95;
			}
		} else {
			if(ghost.getMoveMode() == GameStateManager.NAV_MODE.GO_HOME) {
				movesPerFrame = actorMovesPerFrame * 1.0;
			} else if(objectLocationTracker.objectInTunnel(ghost)) {
				movesPerFrame = actorMovesPerFrame * 0.60;
			} else if(ghost.isScared()) {
				movesPerFrame = actorMovesPerFrame * 0.50;
			} else {
				movesPerFrame = actorMovesPerFrame * 0.95;
			}
		}
		
		ghost.setPixelSpeed(movesPerFrame * 60);
		
		pixelsToMove = (int)Math.floor(movesPerFrame);
		return pixelsToMove + ghost.incrementMovesPerFrameRemainder(movesPerFrame - pixelsToMove);
	}
	
	/*
	 * (Debug use only)
	 * Draw a grid on the screen to visually show the placement and size of each tile on
	 * the game board.
	 * 
	 */
	@SuppressWarnings("unused")
	private void paintGridLines(Graphics g) {
		Graphics2D d = (Graphics2D) g;
		g.setColor(Color.WHITE);
		for(int i = 0; i < 29; i++) {
			d.drawLine((i * windowParams.getPixelScale()) + windowParams.getPixelScale() / 2, 0, (i * windowParams.getPixelScale()) + windowParams.getPixelScale() / 2, windowParams.getWindowHeight());
		}
		
		for(int i = 0; i < 36; i++) {
			d.drawLine(0, (i * windowParams.getPixelScale()) + windowParams.getPixelScale() / 2, windowParams.getWindowWidth(), (i * windowParams.getPixelScale()) + windowParams.getPixelScale() / 2);
		}
		
	}
	
	/*
	 * Draw text that details the current and high score at the top of the JPanel.
	 * 
	 */
	private void displayScore(Graphics g, int currentScore, int highScore) {
		g.setFont(textFont);
		g.setColor(Color.WHITE);
		g.drawString("HIGH SCORE", 11 * windowParams.getPixelScale(), windowParams.getPixelScale());
		if(currentScore == 0) {
			g.drawString("00", 7 * windowParams.getPixelScale(), 2 * windowParams.getPixelScale());
		} else {
			g.drawString(Integer.toString(currentScore), (9 - Integer.toString(currentScore).length()) * windowParams.getPixelScale(), 2 * windowParams.getPixelScale());
		}
		
		if(highScore > 0) {
			g.drawString(Integer.toString(highScore), (18 - Integer.toString(highScore).length()) * windowParams.getPixelScale(), 2 * windowParams.getPixelScale());
		}
	}
	
	
	/*
	 * Draw text that displays a message to the user at the center of the JPanel.
	 * 
	 */
	private void printCenterMessage(Graphics2D g) {
		
		FontRenderContext context = g.getFontRenderContext();
		TextLayout txt = new TextLayout(centerMessage, textFont, context);
		Rectangle2D bounds = txt.getBounds();
		int xString = (int) ((getWidth() - bounds.getWidth()) / 2.0);
		int yString = windowParams.getPixelScale() * 20;
		
		g.setColor(Color.YELLOW);
		g.setFont(textFont);
		g.drawString(centerMessage, xString, yString);
	}
	

	//////////////////////////////////////////////////////////
	// Private member variables
	//////////////////////////////////////////////////////////
	
	private GameStateManager gameStateManager;
	private ObjectLocationTracker objectLocationTracker;
	private ScoreTracker scoreTracker;
	private WindowParameters windowParams;
	private ArrayList<Actor> enemyActors;
	private Ghost blinky, pinky, inky, clyde;
	private PlayerObject playerObject;
	private Font textFont;
	private boolean gamePaused;
	private double actorMovesPerFrame;
	private double playerMovesPerFrame;
	private double ghostMovesPerFrame;
	private int frameCounter;
	private boolean ghostsFlashing;
	private boolean drawLock = false;
	private String centerMessage = " ";
}
